package gr.aueb.cs.mscis.chordroid.app;

import gr.aueb.cs.mscis.chordroid.ChordroidService;
import gr.aueb.cs.mscis.chordroid.chord.NodeIdentifier;
import gr.aueb.cs.mscis.chordroid.util.Sha1;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import android.util.Log;

/**
 * This class handles the uploading of a file to the Chord network.
 * @author lupin
 *
 */
public class FileSeeder {

	private FileApp fileApp;
	private String filename;
	private byte[] data;
	private ConcurrentMap<String,FileData> dataMap;
	private FileDescriptor descriptor;
	private float findSuccessorProgress = 0f;

	/**
	 * Creates a Seeder for a specific file and app, which includes
	 * a {@code FileDescriptor} and the data to be uploaded
	 * @param fileApp
	 * @param filename
	 * @param data
	 */
	public FileSeeder(FileApp fileApp, String filename, byte[] data) {
		this.fileApp = fileApp;
		this.filename = filename;
		this.data = data;
		createDescriptor();
	}

	/**
	 * This method uploads a file. At first the {@code FileDescriptor} is uploaded
	 * and then the {@code FileData} for each replica of the file. The implementation
	 * is done like this: at first findSuccessors calls are fired for every {@code FileChunk}
	 * and then the responses are handled separately.
	 */
	public void uploadFile() {
		
		// upload descriptor
		ChordroidService.getMyLuckyNode().findSuccessor(Sha1.getHash(filename), ChordroidService.getMyLuckyNode().getNodeId());
		NodeIdentifier sendDescrToAddr = getResponseAddr(Sha1.getHash(filename));
		sendFileChunk(descriptor, sendDescrToAddr.getAddr(), sendDescrToAddr.getPort());

		// upload each replica
		for (int i=1; i<=descriptor.getNumOfReplicas(); i++) {
			// split the file
			splitAndHash(i);
			Log.v("FileApp", "after split");
			
			// find the successor for each chunk
			int j=0;
			for (Entry<String, FileData> entry : dataMap.entrySet()) {
				
				final String hash = entry.getKey();
				final int rep = i;
				final int ch = ++j;
//				Thread t = new Thread(new Runnable() {
//					public void run() {
				// vres ton upeu8uno gia ka8e kommati
					successorsProgress(rep, ch);
//					}});
//				t.start();
				ChordroidService.getMyLuckyNode().findSuccessor(hash, ChordroidService.getMyLuckyNode().getNodeId());
			}
		}
				
		// find the node identifiers till now
		Set<NodeIdentifier> nodes = fileApp.getNodes();
		// pack chunks with based on their destination and send them
		for (NodeIdentifier node : nodes) {
			Set<String> hashes = fileApp.getKeysByNode(node);
			Set<FileData> packet = packFileData(hashes);
			sendFileChunk(packet, node.getAddr(), node.getPort());
		}
		
		// fallback if FindSuccessor failed or if is slow
		for (Entry<String, FileData> entry : dataMap.entrySet()) {
			ChordroidService.getMyLuckyNode().findSuccessor(entry.getKey(), ChordroidService.getMyLuckyNode().getNodeId());
			NodeIdentifier sendDataToAddr = getResponseAddr(entry.getKey());
			sendFileChunk(dataMap.get(entry.getKey()), sendDataToAddr.getAddr(), sendDataToAddr.getPort());
		}
		
	}

	/**
	 * Packs multiple {@code FileData} Objects by their destination
	 * @param hashes
	 * @return
	 */
	private Set<FileData> packFileData(Set<String> hashes) {
		Set<FileData> packedData = new HashSet<FileData>();
		for (String hash : hashes) {
			packedData.add(dataMap.get(hash));
			// empty maps
			dataMap.remove(hash);
			fileApp.getHashMap().remove(hash);
		}
		return packedData;
	}

	/**
	 * Creates a new {@code FileDescriptor}
	 */
	private void createDescriptor() {
		descriptor = new FileDescriptor(Sha1.getHash(filename), data.length, Sha1.getHash(data));
	}

	/**
	 * Splits a file based on the meta-data available on {@code FileDescriptor} into
	 * {@code FileData} objects and places them on a datamap
	 * @param replica
	 */
	private void splitAndHash(int replica) {
		dataMap = new ConcurrentHashMap<String, FileData>();
		for (int i=0, j=0; i<data.length; i+=descriptor.getFileChunkSize(), j++) {
			byte[] out;
			if (j == (data.length/descriptor.getFileChunkSize()) ) {
				out = new byte[data.length % descriptor.getFileChunkSize()];
			} else {
				out = new byte[descriptor.getFileChunkSize()];
			}
			System.arraycopy(data, i, out, 0, out.length);
			String hash = Sha1.getHash(filename + "-" + j + "-" + replica);
			dataMap.put(hash, new FileData(hash, out));
		}
	}
	
	/**
	 * Wrapper method for <code>getResponseByHash</code>. This method
	 * checks if a find successor response is available in {@code FileApp}'s
	 * map
	 * @param hash
	 * @return
	 */
	private NodeIdentifier getResponseAddr(String hash) {
		NodeIdentifier successorAddr = fileApp.getResponseByHash(hash);
		int i=0;
		while (successorAddr == null && i < 10) {
			try {
				// relax
				Log.v("seeder","waiting for " + i++ + " time");
				// try again
				successorAddr = fileApp.getResponseByHash(hash);
				Thread.sleep(i*1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		return successorAddr;
	}
	
	/**
	 * Sends a {@code FileChunk} object to the specified Address and the Port of
	 * the remote {@code FileApp} 
	 * @param fileChunk
	 * @param address
	 * @param port
	 */
	private void sendFileChunk(Object fileChunk, InetAddress address, int port) {
		try {
		Log.v("Seeder", "Trying to send to: " + address.toString() + port);
		Socket outToAnotherFileApp = new Socket(address, port + 2); /* i efarmofh akouei se +2 apo to deamon tou chord */
		
		ObjectOutputStream oos = new ObjectOutputStream(outToAnotherFileApp.getOutputStream());
		oos.writeObject(fileChunk); // have to use reflection on the other side

		if (outToAnotherFileApp != null) outToAnotherFileApp.close();
		} catch (IOException e) {
			Log.v("FileSeeder",e.getMessage());
			e.printStackTrace();
		}
	}

	/**
	 * Sets the number of replicas
	 * @param i
	 */
	public void setNumOfReplicasNo(int i) {
		if (i < 1) return;
		createDescriptor();
		descriptor.setNumOfReplicas(i);
	}
	
	/**
	 * Sets the {@code FileChunk}'s size
	 * @param i
	 */
	public void setFileChunkSize(int i) {
		if (i < 1024) return;
		createDescriptor();
		descriptor.setFileSize(i);
	}
	
	/**
	 * Sets the progress of the multiple find successors calls 
	 * @param replicaNo
	 * @param chunkNo
	 */
	private void successorsProgress(int replicaNo, int chunkNo) {
		 setFindSuccessorProgress(((float)chunkNo/dataMap.size()*replicaNo)*100);
	}
	
	/**
	 * setter for the progress of the multiple findsucessor calls
	 * @param progress
	 */
	public void setFindSuccessorProgress(float progress) {
		this.findSuccessorProgress = progress;
	}
	
	/**
	 * gets the progress of the multiple find successor calls
	 * @return
	 */
	public float getFindSuccessorProgress() {
		return this.findSuccessorProgress;
	}


}
